package dev;
/**
 * A state instance oversees event listeners, timeouts, intervals etc.
 * @author Cref
 */
class State {
#if !macro
	
	public static var global = {
		var r = new State(jstm.Host.window,'');
		r.setActive(true);
		r;
	}
	
	var states:Array<State>;
	var listeners:Array<EventListener>;
	var timeouts:IntHash<Int>;
	var intervals:IntHash<Int>;
	public var active(default,null):Bool;
	function setActive(b:Bool):Void {
		if (active == b) return;
		active = b;
		if (b) for (a in on_args) _on(a.t, a.l, a.h);
		else {
			for (t in timeouts) win.clearTimeout(t);
			timeouts = new IntHash();
			for (i in intervals) win.clearInterval(i);
			intervals = new IntHash();
			for (l in listeners) l.target.removeEventListener(l.type, cast l.listener, false);
			listeners = [];
			for (s in states) if (s.active) s.setActive(false);
		}
	}
	
	public function createState(activator:(Bool->Void)->Void,?name:String) {
		var r = new State(win,name);
		activator(r.setActive);
		states.push(r);
		if (!active) r.setActive(false);
		return r;
	}
	
	function new(w:Window,name:String) {
		win = w;
		this.name = name;//todo: check for doubles
		states = [];
		listeners = [];
		timeouts = new IntHash();
		intervals = new IntHash();
		on_args = [];
	}
	
	var win:Window;
	//the name used for synchronized url arguments: name.var=value
	var name:String;
	
	//clear all on inactive
	var on_args:Array<{t:Trigger,l:Void->Void, h:Int}>;
	
	public function on(trigger:Trigger, listener:Void->Void, ?holdMsec:Int):State/*{scope:State,cancel:Void->Void}*/ { //TODO: max:Int
		on_args.push( { t:trigger, l:listener, h:holdMsec } );
		if (active) _on(trigger,listener,holdMsec);
		return this;
	}
	
	function _on(trigger:Trigger, listener:Void->Void, ?holdMsec:Int) {
		if (holdMsec != null) {
			var fn = listener, t = this, isOnHold = false, cancelOnHold = function() {
				fn();
				isOnHold=false;
			}
			listener = function() {
				if (isOnHold) return;
				isOnHold = true;
				t.setTimeout(cancelOnHold, holdMsec);
			}
		}
		//seperate function setListener because of Trigger.any iteration
		setListener(trigger,listener);
	}
	
	function setListener(trigger:Trigger, listener:Void->Void) {
		switch(trigger) {
			case before(fn):setCallListener(fn,listener,true);
			case after(fn):setCallListener(fn, listener, false);
			case event(target, type):target.addEventListener(type, cast listener, false);
			case poll(fn, msec, continuous):
				var id = null,t=this;
				id=setInterval(function() {
					if (fn()) {
						if (!continuous) t.clearInterval(id);//TODO: move continuous to max
						listener();
					}
				},msec);
			case timeout(msec):setTimeout(listener,msec);
			case interval(msec):setInterval(listener,msec);
			case repaint://TODO
			case repaintElement(elm)://TODO
			case immediate:setTimeout(listener,0);//TODO//untyped win.setImmediate(listener);//not part of W3C standard yet
			case any(triggers): for (t in triggers) setListener(t, listener);
		}
	}
	
	//TODO: args
	public function setInterval(fn:Void->Void,msec:Int):Int {
		var id = win.setInterval(fn, msec);
		intervals.set(id,id);
		return id;
	}
	
	public function clearInterval(id:Int) {
		intervals.remove(id);
		win.clearInterval(id);
	}
	
	//TODO: args
	public function setTimeout(fn:Void->Void,msec:Int):Int {
		var t=timeouts,id = null;
		t.set(id=win.setTimeout(function() {
			t.remove(id);
			fn();
		}, msec), id);
		return id;
	}
	
	public function clearTimeout(id:Int) {
		timeouts.remove(id);
		win.clearTimeout(id);
	}
	
	function setCallListener(fn1:Dynamic,fn2:Void->Void,doBefore:Bool) {
		var c:Closure = cast fn1,t=this;
		var evtId = hxtc.Tools.getInstanceId(c.scope) + '.' + c._name,fn=c.scope[cast c._name];
		if (!fn._isDispatcher) {
			//wrap the function in an event dispatcher
			var dispatcher = function() {
				//still considering on whether to pass on the function arguments and allowing stopPropagation
				if (!t.win.dispatchEvent(t.createEvent('b'+evtId))) return;
				var r = fn.apply(c.scope, ES5.arguments);
				//still considering on whether to pass on the function arguments and return value
				t.win.dispatchEvent(t.createEvent('a'+evtId));
				return r;
			};
			untyped dispatcher._isDispatcher = true;
			c.scope[cast c._name] = dispatcher;
		}
		setListener(event(win,(doBefore?'b':'a') + evtId),fn2);
	}
	
	public function loader<T>(callbck:(T->Void)->Void) {
		var callbacks = [], t = this, use = null;
		use=function(cb) {
			use=function(cb) callbacks.push(cb);
			use(cb);
			callbck(function(result:T) {
				use = function(cb) t.win.setTimeout(function() cb(result), 0);//TODO: setImmediate
				for (cb in callbacks) use(cb);
			});
		}
		return function(cb) use(cb);
	}
	
	public function sync<T>(p1:Property<T>, p2:Property<T>,stallMsec=100) {
		on(after(p1.change),function() p2.value = p1.value,stallMsec);
		on(after(p2.change),function() p1.value = p2.value,stallMsec);
	}
	
	function createEvent(type:String):Dynamic {
		var event = win.document.createEvent('Event');
		event.initEvent(type, false, true);
		return event;
	}
#end
}

enum Trigger {
	before(fn:Dynamic);
	after(fn:Dynamic);
	event(target:org.w3c.dom.events.EventTarget, type:String);
	poll(fn:Void->Bool, interval:Int, ?continuous:Bool);
	timeout(msec:Int);
	interval(msec:Int);
	repaint;
	repaintElement(elm:HTMLElement);
	immediate; //https://developer.mozilla.org/en/DOM/window.setImmediate
	any(triggers:Array<Trigger>);
}

private typedef TimedListener = {
	id:Int,
	msec:Int,
	listener:Void->Void
}

private typedef EventListener = {
	target:org.w3c.dom.events.EventTarget,
	type:String,
	listener:Void->Void
}


private typedef Closure = { scope:Dynamic, method:Function<Dynamic,Dynamic,Dynamic>, _name:String }